使用者除了有匯出報表的需求外,也會有需要大量匯入的情境,匯入會更需要驗證輸入的資料,有可能是空的資料、跟預期輸入 Excel 完全不同、重複的資料 (需看情境是否接受重複的資料)、部分資料輸入錯誤...等
這邊會需要使用到 roo 和 roo-xls 這 2 個 Gem ,可參考此 pr
# Gemfile
gem 'roo', '~> 2.8', '>= 2.8.3'
gem 'roo-xls', '~> 1.2'
以下範例有做簡單的驗證,像是判斷輸入的 Excel 標題列是否一樣、是否有資料、是否有重複的資料、是否與 DB 資料重複
大量匯入時,可改用 activerecord-import
處理,可參考這篇文章
# app/services/shops_excel/parser.rb
module ShopsExcel
class ParseError < StandardError; end
class Parser
def execute(file_path)
list = read_excel(file_path)
sheet = list.sheet(0)
validate_title_names!(sheet)
validate_shop_names!(sheet)
process_shops(sheet)
rescue ParseError => e
subject = "[#{self.class} Error] #{e.message}, \nbacktrace: #{e.backtrace}"
Rails.logger.error subject
end
private
def read_excel(file_path)
Roo::Spreadsheet.open(file_path)
end
def validate_title_names!(sheet)
return if sheet.row(1) == ShopsExcel::Generator::TITLES
raise ParseError, '輸入資料有誤,比對 Excel 標頭與預期不同'
end
def validate_shop_names!(sheet)
all_shop_names = sheet.column(1)[1..-1]
raise ParseError, '無資料' if all_shop_names.blank?
raise ParseError, '有重複的商家名稱,請檢查' if all_shop_names.uniq.size != all_shop_names.size
existed_shop_names = Shop.where(name: all_shop_names).pluck(:name)
return if existed_shop_names.blank?
raise ParseError, "有 #{existed_shop_names.size} 筆已建立過: #{existed_shop_names.join(', ')}"
end
def process_shops(sheet)
shops = []
(2..sheet.last_row).each do |index|
col_values = sheet_row(sheet, index)
shop = Shop.new(col_values)
shops << shop
end
Shop.import(shops)
end
def sheet_row(sheet, index)
{
name: sheet.row(index)[0], # 商家名稱
email: sheet.row(index)[1], # 信箱
note: sheet.row(index)[2], # 備註
}
end
end
end
在 rails console
輸入以下
# rails console
file = Rails.root.join("data/商家總表.xlsx").to_s
ShopsExcel::Parser.new.execute(file)
匯入要做的基本驗證蠻多的,需考量各種情境,並盡可能地去預防處理,和匯出一樣,建議都是放 sidekiq 處理,完成後再寄信給使用者,告知匯入結果
鐵人賽文章連結:https://ithelp.ithome.com.tw/articles/10272591
medium 文章連結:https://link.medium.com/Teq2MzayRjb
本文同步發布於 小菜的 Blog https://riverye.com/
備註:之後文章修改更新,以個人部落格為主